Data Access & Storage

SQL Server 2022 - Neue Features

Die T-SQL-Neuerungen in der Praxis

Thorsten Kansy

Seit nicht ganz einem Jahr ist SQL Server 2022 die neueste Version von Microsofts Datenbankflaggschiff. Zeit also, die Neuerungen im Bereich T-SQL mal ein wenig unter die Lupe zu nehmen und zu schauen, welche von ihnen tatsächlich Verbesserungen im Projekt mit sich bringen und welche eher unbedeutend sind. Im Vergleich zu den direkten Vorgängern sind die Neuerungen auf jeden Fall umfangreicher.

Neben den reinen Neuerungen in komprimierter Form erwarten interessierte Leser und Leserinnen also auch ein – sicherlich persönlich geprägtes – Minifazit. Sollte dies von den eigenen Anforderungen oder der eigenen Meinung abweichen, so mögen Sie es dem Autor verzeihen. Kenntnisse älterer SQL-Server-Versionen sind hilfreich.

Unterscheidbar oder nicht? Filtern mit NULL

Zu prüfen, ob zwei Werte gleich sind oder nicht, klingt im ersten Moment trivial. Und zwar genau so lange, bis einer (oder gar beide) NULL sein können. Ab diesem Moment muss mit einer Kombination von gleich und ungleich sowie mit dem IS-Operator gearbeitet werden. (Zur Erinnerung: NULL = NULL OR NULL <> NULL ist immer falsch, auch wenn es dem einen oder anderen erst einmal widersinnig erscheint). Aus diesem Grund bietet SQL Server 2022 nun die Syntax A IS DISTINCT FROM B beziehungsweise A IS NOT DISTINCT FROM B. Eine Abfrage kann so aussehen:

 

SELECT * FROM <Tabelle/ Sicht> WHERE A IS DISTINCT FROM B;

SELECT * FROM <Tabelle/ Sicht> WHERE A IS NOT DISTINCT FROM B;

 

Abbildung 1 zeigt, wie der Server dies umsetzt – etwas umfangreich, aber korrekt und nötig. Sicher ist, dass es sich bei A/B um Ausdrücke oder Spalten handeln kann.

 

Abb. 1: Decoding der neuen IS (NOT) DISTINCT FROM-Syntax

 

Minifazit: Wer bereit ist, statt A=B einfach A IS NOT DISTINCT B zu schreiben, bekommt ein Ergebnis, das in jedem Fall korrekt ist. Mehr ist nicht nötig. Notlösungen mit der ISNULL()-Funktion sollten damit endgültig der Vergangenheit angehören.

RTRIM(), LTRIM()? Besser TRIM()!

Das Entfernen von Zeichen von Anfang und/oder Ende einer Zeichenkette ist eine Grundfunktionalität, für die es eigentlich für jedes Framework eine Lösung gibt. Nun auch für T-SQL mit der Angabe, welche Zeichen entfernt werden sollen. Whitespaces sind also nicht mehr Pflicht. Außerdem kann nun für die TRIM()-Funktion bestimmt werden, ob sie den Anfang oder das Ende einer Zeichenkette berücksichtigen soll oder beides. Letzteres wird nicht über einen Parameter, sondern den Zusatz LEADING, TAILING oder BOTH gesteuert. Listing 1 zeigt, wie das genau aussieht.

 

ZUM NEWSLETTER

Regelmäßig News zur Konferenz und der .NET-Community

 

Listing 1
DECLARE @caption VARCHAR(100) = '-== SQL Server 2022 ==-';


SELECT LTRIM(@caption, '-= ') AS [LTRIM],

       RTRIM(@caption, '-= ') AS [RTRIM];


SELECT TRIM(LEADING '-= ' FROM @caption) AS [LEADING],

       TRIM(TRAILING '-= ' FROM @caption) AS [TRAILING],

       TRIM(BOTH '-= ' FROM @caption) AS [BOTH];

 

 

Minifazit: Praktisch, dass genau bestimmt werden kann, welche Zeichen (‘-= ‘ in Listing 1) entfernt werden sollen. Nur an die neue Syntax der TRIM()-Funktion muss man sich sicherlich erst gewöhnen.

Horizontales Maxi-/Minimum

Um den größten beziehungsweise kleinsten Wert einer Reihe von Werten und Ausdrücken zu ermitteln, waren bisweilen „interessante“ Lösungen notwendig. Die dabei naheliegenden Aggregate MIN() und MAX() funktionieren nicht, da diese nur einen Parameter akzeptieren und vertikal (aus Spalten) arbeiten. GREATEST() und LEAST() kann man nun eine Menge an kompatiblen Werten übergeben und erhält den größten oder den kleinsten von diesen. Folgender Abschnitt zeigt ein paar Aufrufe und das daraus folgende Ergebnis:

 

SELECT GREATEST('6.62', 3.1415, N'7') AS 'GREATEST', -- 7.0000

       LEAST( '6.62', 3.1415, N'7') AS 'LEAST'; -- 3.1415




SELECT GREATEST('A', 'B', 'C') AS 'GREATEST', -- C

       LEAST('A', 'B', 'C') AS 'LEAST'; -- A

 

Minifazit: Klein, aber sehr hilfreich. Wer hatte so eine Anforderung noch nicht?

Zahlenreihen erzeugen

Ebenfalls öfter mal benötigt und nun leicht zu erzeugen: eine Zahlenreihe von einem Start- bis zu einem Endwert; auf Wunsch auch mit Schrittweite. Damit ist der konkrete Datentyp unerheblich, so lange er nur numerisch ist oder als solcher interpretiert wird. In der Praxis bedeutet dies Zahlen mit oder ohne Komma. Beides akzeptiert die neue GENERATE_SERIES()-Funktion. Neben den obligatorischen Werten für Start und Ende ist der Wert für die Schrittweite optional und SQL Server verwendet ohne Angabe 1, bzw. -1. Listing 2 zeigt ein paar Aufrufe mit der Funktion.

 

Listing 2

-- Von 1 bis <=50 in 5er Schritten => 1..6..11..41..46

SELECT value FROM GENERATE_SERIES(/* Start */ 1, /*STOP*/ 50, /*STEP*/ 5);




-- Auch andere (numerische) Datentypen sind erlaubt

SELECT value FROM GENERATE_SERIES(/* Start */ 1.0, /*STOP*/ 10.0, /*STEP*/ .5);




-- Alle Kalenderwochen 2022

SELECT CAST(DATEADD(WEEK, value - 1, '2022-01-03') AS DATE), value FROM GENERATE_SERIES(1, 52);

 

 

Soll eine Serie mit anderen Datentypen erzeugt werden, so ist das auf Basis der Zahlenreihe natürlich auch kein Problem, wie die letzte Abfrage aus Listing 2 zeigt. Die Ausgabe ist in Abbildung 2 zu sehen.

 

Abb. 2: Kalenderwochen mit der GENERATE_SERIES()-Funktion

 

Minifazit: Ziemlich hilfreiche kleine Funktion für vielfältige Einsätze.

DATETRUNC()

Die neue DATETRUNC()-Funktion schneidet alle Details eines Datums/einer Uhrzeit ab der übergebenen Einheit ab. So liefert DATETRUNC(YEAR, ‘2022-10-25 19:45:32’) schlicht ‘2022-01-01 00:00:00.0000000’. Für Month wäre es ‘2022-10-01 00:00:00.0000000’. Listing 3 zeigt einige Varianten mit deren jeweiligen Ausgaben.

 

Listing 3

DECLARE @d DATETIME2 = GETDATE();

SELECT 'Year', DATETRUNC(YEAR, @d); -- 2022-01-01 00:00:00.0000000

SELECT 'Quarter', DATETRUNC(QUARTER, @d); --2022-10-01 00:00:00.0000000

SELECT 'Month', DATETRUNC(MONTH, @d); -- 2022-12-01 00:00:00.0000000

SELECT 'Week', DATETRUNC(WEEK, @d); -- 2022-12-11 00:00:00.0000000

SELECT 'Iso_week', DATETRUNC(ISO_WEEK, @d); -- 2022-12-12 00:00:00.0000000

SELECT 'DayOfYear', DATETRUNC(DAYOFYEAR, @d); -- 2022-12-17 00:00:00.0000000

SELECT 'Day', DATETRUNC(DAY, @d); -- 2022-12-17 00:00:00.0000000

SELECT 'Hour', DATETRUNC(HOUR, @d); -- 2022-12-17 19:00:00.0000000

SELECT 'Minute', DATETRUNC(MINUTE, @d); -- 2022-12-17 19:45:00.0000000

SELECT 'Second', DATETRUNC(SECOND, @d); -- 2022-12-17 19:45:17.0000000

SELECT 'Millisecond', DATETRUNC(MILLISECOND, @d); -- 2022-12-17 19:45:17.5800000

SELECT 'Microsecond', DATETRUNC(MICROSECOND, @d); -- 2022-12-17 19:45:17.5800000

 

 

Minifazit: Diese Funktion sorgt für einfacheren und lesbareren Code, wenn ein vollständiges Datum oder eine vollständige Uhrzeit vorliegt (z. B. von GETDATE() oder GETUTCDATE()), aber nur ein Teil davon tatsächlich benötigt wird.

SELECT … WINDOW-Klausel

Wer viel mit Window-Funktionen oder Aggregaten bei seinen Abfragen arbeitet, wird diese kleine Verbesserung zu schätzen wissen, da sich so repetitiver Code vermeiden lässt. Mit der WINDOW-Klausel lässt sich PARTITION BY und ORDER BY pro Abfrage einmal definieren, mit einem Namen versehen und dann mehrfach verwenden. Statt also eine Abfrage wie in Listing 4 ist nun eine wie in Listing 5 möglich.

 

Listing 4

SELECT SalesOrderID, ProductID, OrderQty

  ,SUM(OrderQty) OVER (PARTITION BY SalesOrderID) AS Total

  ,AVG(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Avg"

  ,COUNT(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Count"

  ,MIN(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Min"

  ,MAX(OrderQty) OVER (PARTITION BY SalesOrderID) AS "Max"

  FROM Sales.SalesOrderDetail

WHERE SalesOrderID IN(43659,43664);


 

Listing 5

SELECT SalesOrderID, ProductID, OrderQty

  ,SUM(OrderQty) OVER win AS Total

  ,AVG(OrderQty) OVER win AS "Avg"

  ,COUNT(OrderQty) OVER win AS "Count"

  ,MIN(OrderQty) OVER win AS "Min"

  ,MAX(OrderQty) OVER win AS "Max"

FROM Sales.SalesOrderDetail

WHERE SalesOrderID IN(43659,43664)

WINDOW win AS (PARTITION BY SalesOrderID);

 

 

Die Abfrage in Listing 5 ist kompakter und Fehler bei Änderungen haben ebenfalls weniger Chancen.

Minifazit: Praktisch, wenn man viel mit umfangreichen Window-Funktionen oder Aggregaten arbeitet. Sonst eher hinderlich; dafür handelt es sich aber auch nur um eine Option, die alte Syntax existiert natürlich nach wie vor.

FIRST_VALUE() und LAST_VALUE()

Die Funktionen FIRST()_VALUE und LAST_VALUE() sind bei weitem nicht neu, sondern schon seit SQL Server 2012 im Portfolio. Nun sind sie um eine Syntax erweitert worden, die steuert, ob NULL berücksichtigt wird (RESPECT NULLS) oder nicht (IGNORE NULLS). Listing 6 zeigt beide nun möglichen Varianten.

 

Listing 6

SELECT Name, ListPrice,

       FIRST_VALUE(Name) RESPECT NULLS OVER (ORDER BY ListPrice ASC) AS LeastExpensive

FROM Production.Product

WHERE ProductSubcategoryID = 37;

SELECT Name, ListPrice,

       FIRST_VALUE(Name) IGNORE NULLS OVER (ORDER BY ListPrice ASC) AS LeastExpensive

FROM Production.Product

WHERE ProductSubcategoryID = 37;

 

 

Minifazit: Die neue Syntax erspart eine zusätzliche Unterabfrage und macht damit die Abfrage an sich einfacher, lesbarer und performanter. Wer die beiden Funktionen bis dato noch nicht verwenden konnte, wird allerdings auch nicht bekehrt werden.

 

 

STRING_SPLIT() mit Ordinal (Index)

Auch die STRING_SPLIT()-Funktion ist nicht neu, wurde aber mit SQL Server 2022 um einen optionalen Parameter erweitert, der neben den aufgeteilten Elementen der Zeichenketten auch den entsprechenden Indexwert (beginnend mit 1) in einer zusätzlichen Spalte liefert –  zumindest wenn eine 1 als Wert übergeben wird. Die folgende Abfrage liefert das Ergebnis, das in Abbildung 3 gezeigt wird:

 

SELECT * FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' ', 1);

 

Abb. 3: STRING_SPLIT()-Funktion mit Ordinal (Index)

 

Minifazit: Wahrlich eine Detailerweiterung mit wenig Einsatzpotenzial, oder?

Genäherte Aggregate

An sich sind die grundlegenden Funktionen PERCENTILE_CONT() und PERCENTILE_DISC() ebenfalls nicht neu. APPROX_PERCENTILE_CONT() und APPROX_PERCENTILE_DISC() sind nun Varianten, die auch bei größeren Datenmengen sehr performant sind, dafür aber nur genähert und so mit einer Abweichung versehen sind. Listing 7 zeigt diese beiden Funktionen.

 

Listing 7

-- Kontinuierliche Werte

SELECT DeptId,

       APPROX_PERCENTILE_CONT(0.10) WITHIN GROUP(ORDER BY Salary) AS 'P10',

       APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP(ORDER BY Salary) AS 'P90'

FROM #Employee

GROUP BY DeptId;




-- Diskrete Werte

SELECT DISTINCT DeptId,

       PERCENTILE_DISC(0.10) WITHIN GROUP (ORDER BY Salary) OVER (PARTITION BY DeptId) AS 'P10',

       PERCENTILE_DISC(0.90) WITHIN GROUP (ORDER BY Salary) OVER (PARTITION BY DeptId) AS 'P90' 

FROM #Employee;

 

 

Minifazit: Ebenfalls eine Detailerweiterung für die wenigen, die ausgiebig mit PERCENTILE_CONT() und PERCENTILE_DISC() arbeiten – für den Rest eher uninteressant.

Zeitliche Abschnitte mit der DATE_BUCKET()-Funktion

Für das Erzeugen von zeitlichen Abschnitten steht ab SQL Server 2022 die DATE_BUCKET()-Funktion bereit. Sie liefert den Anfang eines Abschnittes (Woche, Monat, Jahr etc.) basierend auf einem Datum. Und dabei muss es sich nicht um den ersten Abschnitt handeln, er kann auch per Index bestimmt werden. Listing 8 zeigt den Aufruf.

 

Listing 8

DECLARE @date DATETIME2 = '2022-02-24 00:00:00';

SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date); -- 2022-02-21 00:00:00.0000000

SELECT 'MONTH', DATE_BUCKET(MONTH, 1, @date); -- 2022-02-21 00:00:00.0000000

SELECT 'YEAR', DATE_BUCKET(YEAR, 1, @date); -- 2022-01-01 00:00:00.0000000




DECLARE @date DATETIME2 = '2022-02-24 00:00:00';

DECLARE @origin1 DATETIME2 = '2021-01-01 00:00:00';

DECLARE @origin2 DATETIME2 = '2021-02-22 00:00:00';

SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date, @origin1); -- 2022-02-18 00:00:00.0000000

SELECT 'WEEK', DATE_BUCKET(WEEK, 1, @date, @origin2); -- 2022-02-21 00:00:00.0000000

Minifazit: Eine sehr spezielle Funktion, die für viele keine Rolle spielen wird.

Bit-Funktionen

Gleich eine Handvoll neuer Funktionen für die Arbeit mit Bit-Werten bietet SQL Server 2022, auch wenn es wenige Gelegenheiten gibt, im T-SQL-Umfeld damit zu arbeiten – die UPDATE()-Funktion im Zuge von DML-Triggern ist eine der wenigen davon. Tabelle 1 zeigt die einzelnen Funktionen zusammen mit einer kleinen Erläuterung. Listing 9 zeigt die Verwendung dieser Funktionen in einem kurzen Beispiel.

 

Funktion Erläuterung
RIGHT_SHIFT() Verschiebt den numerischen Wert binär nach rechts
LEFT_SHIFT() Verschiebt den numerischen Wert binär nach links
BIT_COUNT () Liefert die Anzahl der (aus 1) gesetzten Bits
GET_BIT() Liefert den Status (0 oder 1) eines bestimmten Bits
SET_BUT() Setzt den Status (0 oder 1) eines bestimmten Bits

Tabelle 1: Neue Bit-Funktionen

 

Listing 9

-- RIGHT_SHIFT()/ LEFT_SHIFT()

SELECT @number, RIGHT_SHIFT(@number, 1) '>> 1', RIGHT_SHIFT(@number, 2) '>> 2', RIGHT_SHIFT(@number, 3) '>> 3'; -- 10011010010   1001101001  100110100  10011010

SELECT @number, LEFT_SHIFT(@number, 1) '<< 1', LEFT_SHIFT(@number, 2) '<< 2', LEFT_SHIFT(@number, 3) '<< 3'; -- 10011010010  100110100100  1001101001000  10011010010000




-- BIT_COUNT()

SELECT @number, BIT_COUNT(@number) 'Number of 1s'; -- 10011010010  5




-- SET_BIT()/ GET_BIT()

SELECT GET_BIT(@number, 0) 'Pos 0', GET_BIT(@number, 1) 'Pos 1';

 

 

Minifazit: Wie bereits erwähnt, gibt es nur wenige Stellen, an denen in T-SQL mit Bit-Operationen sinnvoll gearbeitet werden kann. Aber wenn man es mit so einer Stelle zu tun hat, machen die Neuerungen vieles einfacher.

Arbeiten mit JSON

Und vielleicht das Beste zum Schluss: Arbeiten mit JSON ist seit SQL Server 2016 möglich und wird nun mit SQL Server 2022 essenziell erweitert. Um genau zu sein, gibt es drei neue Funktionen und eine Erweiterung einer bestehenden Funktion. Tabelle 2 zeigt diese Funktionen mit einer kleinen Erläuterung für ihren jeweiligen Zweck. Einige Funktionen arbeiten dabei mit unterschiedlichen JSON-Typen, die in Tabelle 3 aufgelistet sind. Listing 10 zeigt den Einsatz der JSON-Funktion im Beispiel.

 

Funktion Zweck
JSON_ARRAY() Erzeugt ein JSON-Array (Neu)
JSON_OBJECT() Erzeugt ein einfaches JSON-Objekt (Neu)
ISJSON() Erweitert um die neue JSON-Typangabe
JSON_PATH_EXIST() Prüft, ob ein JSON-Objekt den angegebenen Pfad besitzt (Neu)

Tabelle 2: Neue und erweiterte JSON-Funktionen

 

JSON-Type Beschreibung
VALUE Testet auf einen Wert (object, array, number, string, true, false, null
ARRAY Testet auf ein JSON-Array
OBJECT Testet auf ein JSON-Objekt
SCALAR Testet auf einen gültigen Skalarwert – Zahl, String etc.

Tabelle 3: Unterstützte JSON-Typen

 

ZUM NEWSLETTER

Regelmäßig News zur Konferenz und der .NET-Community

 

Listing 10

DECLARE @jsonInfo NVARCHAR(MAX)=N'{"info":{"address":[{"town":"Nidderau-Erbstadt"},{"town":"Rom"}]}}';




-- JSON_ARRAY

SELECT JSON_ARRAY();

SELECT JSON_ARRAY('a', 1, 'b', 2);

SELECT JSON_ARRAY('name', name, 'id', database_id) FROM sys.databases;

GO




-- JSON_OBJECT

SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3);

SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3 NULL ON NULL /* Standard */ );

SELECT JSON_OBJECT('name':'value', 'type':null, 'abc': 3 ABSENT ON NULL);

GO




-- ISJSON

DECLARE @json VARCHAR(MAX) = '["a",1,"b",2]';

SELECT ISJSON(@json, ARRAY); -- 1

SELECT ISJSON(@json, SCALAR); -- 0

-- ARRAY, SCALAR, VALUE, OBJECT

GO




-- JSON_PATH_EXISTS

DECLARE @jsonInfo NVARCHAR(MAX)=N'{"info":{"address":[{"town":"Nidderau-Erbstadt"},{"town":"Rom"}]}}';

SELECT JSON_PATH_EXISTS(@jsonInfo,'$.info.address'); -- 1

SELECT JSON_PATH_EXISTS(@jsonInfo,'$.info.email'); -- 0

GO


 

Minifazit: Sinnvolle und nützliche Funktionen und Erweiterungen, für die man bei der Arbeit mit JSON schnell einen Einsatz findet. Leider ist (immer noch) kein JSON-Datentyp vorhanden, um mit großen Datenmengen im JSON-Format performant arbeiten zu können.

Gesamtfazit

SQL Server 2022 bietet einiges Neue für T-SQL: IS [not ]DISTINCT FROM, RTRIM(), LTRIM() & TRIM(), GREATEST() & LEAST() und DATETRUNC() sind nützliche Verbesserungen, die schnell Verwendung finden. Andere Neuerungen sind recht speziell, finden aber bestimmt auch ihre Anwender:innen. Insgesamt sicher keine echte Revolution, aber eine Evolution ist es schon. Vielleicht schafft es Microsoft sogar noch, T-SQL sprachlich in die Zukunft zu führen.

Top Articles About Data Access & Storage

Ihr aktueller Zugang zur .NET- und Microsoft-Welt.
Der BASTA! Newsletter:

Behind the Tracks

.NET Framework & C#
Visual Studio, .NET, Git, C# & mehr

Agile & DevOps
Agile Methoden, wie Scrum oder Kanban und Tools wie Visual Studio, Azure DevOps usw.

Web Development
Alle Wege führen ins Web

Data Access & Storage
Alles rund um´s Thema Data

JavaScript
Leichtegewichtig entwickeln

UI Technology
Alles rund um UI- und UX-Aspekte

Microservices & APIs
Services, die sich über APIs via REST und JavaScript nutzen lassen

Security
Tools und Methoden für sicherere Applikationen

Cloud & Azure
Cloud-basierte & Native Apps

NIE MEHR BASTA! NEWS VERPASSEN